<template>
  <el-card style="width: 60%; margin: 80px auto" header="文件上传">
    <el-upload
        drag
        action="/"
        show-file-list
        :http-request="handleHttpRequest"
        :on-remove="handleRemoveFile"
        style="margin-left: 180px">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处，或<em>点击上传</em></div>
    </el-upload>
  </el-card>
</template>

<script setup>

import md5 from '@/lib/md5'
import { taskInfo, initTask, preSignUrl, merge } from '@/api/upload/minioApi'
import Queue from 'promise-queue-plus'
import axios from 'axios'
import { ref } from 'vue'

// 文件上传分块任务的队列（用于移除文件时，停止该文件的上传队列） {key: fileUid, value: queue object}
const fileUploadChunkQueue = ref({}).value

/**
 * 获取一个上传任务，没有则初始化一个
 */
const getTaskInfo = async (file) => {
  let task
  const identifier = await md5(file)
  const {
    code,
    data
  } = await taskInfo(identifier)
  if (code === 10000) {
    task = data
    if (!task) {
      const initTaskData = {
        identifier,
        fileName: file.name,
        totalSize: file.size,
        chunkSize: 5 * 1024 * 1024
      }
      const {
        code,
        data
      } = await initTask(initTaskData)
      if (code === 10000) {
        task = data
      } else {
        this.$message.error('文件上传失败')
      }
    }
  } else {
    this.$message.error('文件上传失败')
  }
  // 打印文件远程URL
  console.log(task.path)
  return task
}

/**
 * 上传逻辑处理，如果文件已经上传完成（完成分块合并操作），则不会进入到此方法中
 */
const handleUpload = (file, taskRecord, options) => {
  let uploadedSize = 0 // 已上传的大小
  const totalSize = file.size || 0 // 文件总大小
  const {
    exitPartList,
    chunkSize,
    chunkNum,
    fileIdentifier
  } = taskRecord
  const uploadNext = async (partNumber) => {
    const start = Number(chunkSize) * (partNumber - 1)
    const end = start + Number(chunkSize)
    const blob = file.slice(start, end)
    const {
      code,
      data
    } = await preSignUrl({
      identifier: fileIdentifier,
      partNumber: partNumber
    })
    if (code === 10000 && data) {
      await axios.request({
        url: data,
        method: 'PUT',
        data: blob,
        headers: {
          'Content-Type': 'application/octet-stream'
        }
      })
      return Promise.resolve({
        partNumber: partNumber,
        uploadedSize: blob.size
      })
    }
    return Promise.reject(`分片${partNumber}， 获取上传地址失败`)
  }

  /**
   * 更新上传进度
   * @param increment 为已上传的进度增加的字节量
   */
  const updateProcess = (increment) => {
    increment = Number(increment)
    const { onProgress } = options
    let factor = 1000 // 每次增加 1000byte
    let from = 0
    // 通过循环一点一点的增加进度
    while (from <= increment) {
      from += factor
      uploadedSize += factor
      const percent = Math.round(uploadedSize / totalSize * 100).toFixed(2)
      onProgress({ percent: percent })
    }
  }
  return new Promise(resolve => {
    const failArr = []
    const queue = Queue(5, {
      'retry': 3,
      'retryIsJump': false,
      'workReject': function (reason) {
        failArr.push(reason)
      },
      'queueEnd': function () {
        resolve(failArr)
      }
    })
    fileUploadChunkQueue[file.uid] = queue
    for (let partNumber = 1; partNumber <= chunkNum; partNumber++) {
      const exitPart = (exitPartList || []).find(exitPart => exitPart.partNumber === partNumber)
      if (exitPart) {
        // 分片已上传完成，累计到上传完成的总额中
        updateProcess(exitPart.size)
      } else {
        queue.push(() => uploadNext(partNumber).then(res => {
          // 单片文件上传完成再更新上传进度
          updateProcess(res.uploadedSize)
        }))
      }
    }
    if (queue.getLength() === 0) {
      // 所有分片都上传完，但未合并，直接return出去，进行合并操作
      resolve(failArr)
      return
    }
    queue.start()
  })
}

/**
 * el-upload 自定义上传方法入口
 */
const handleHttpRequest = async (options) => {
  const file = options.file
  const task = await getTaskInfo(file)
  if (task) {
    const {
      finished,
      path,
      taskRecord
    } = task
    const { fileIdentifier: identifier } = taskRecord
    if (finished) {
      return path
    } else {
      const errorList = await handleUpload(file, taskRecord, options)
      if (errorList.length > 0) {
        this.$message.error('部分文件分片上传失败，请尝试重新上传文件')
        return
      }
      const {
        code
      } = await merge(identifier)
      if (code === 10000) {
        return path
      } else {
        this.$message.error('文件上传失败')
      }
    }
  } else {
    this.$message.error('获取上传任务失败')
  }
}

/**
 * 移除文件列表中的文件
 * 如果文件存在上传队列任务对象，则停止该队列的任务
 */
const handleRemoveFile = (uploadFile) => {
  const queueObject = fileUploadChunkQueue[uploadFile.uid]
  if (queueObject) {
    queueObject.stop()
    fileUploadChunkQueue[uploadFile.uid] = undefined
  }
}

</script>
